// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace GSpec.Framework
{
	using System;
	using System.Collections.Generic;
	using System.Diagnostics;

	using Gallio.Common.Collections;
	using Gallio.Common.Reflection;
	using Gallio.Framework;
	using Gallio.Framework.Pattern;
	using Gallio.Model;

	[AttributeUsage(PatternAttributeTargets.ContributionMethod, AllowMultiple = false, Inherited = true)]
	public sealed class WhenAttribute : ContributionMethodPatternAttribute
	{
		public static readonly Key<Exception> WhenExceptionKey = new Key<Exception>("WhenException");

		/// <inheritdoc />
		protected override void DecorateContainingScope(IPatternScope containingScope, IMethodInfo method)
		{
			containingScope.TestBuilder.TestInstanceActions.DecorateChildTestChain.After(
				(state, decoratedChildActions) =>
				decoratedChildActions.TestInstanceActions.ExecuteTestInstanceChain
					.Around((instanceState, inner) => this.ExecuteTestInstance(inner, instanceState, method)));
		}

		private void ExecuteTestInstance(
			Action<PatternTestInstanceState> inner, PatternTestInstanceState state, IMethodInfo method)
		{
			ExecuteFixtureMethod(state, method);

			inner(state);
		}

		private void ExecuteFixtureMethod(PatternTestInstanceState state, IMethodInfo method)
		{
			string expectedExceptionType = state.TestStep.Metadata.GetValue(MetaKeys.WhenException)
					?? state.Test.Metadata.GetValue(MetaKeys.WhenException);

			if (expectedExceptionType == null)
			{
				state.InvokeFixtureMethod(method, EmptyArray<KeyValuePair<ISlotInfo, object>>.Instance);

				return;
			}

			try
			{
				state.InvokeFixtureMethod(method, EmptyArray<KeyValuePair<ISlotInfo, object>>.Instance);

				using (TestLog.Failures.BeginSection("Expected Exception"))
					TestLog.Failures.WriteLine("Expected an exception of type '{0}' but none was thrown.", expectedExceptionType);
			}
			catch (Exception ex)
			{
				Type exceptionType = ex.GetType();

				if (ReflectionUtils.IsAssignableFrom(expectedExceptionType, exceptionType))
				{
					TestContext.CurrentContext.Data.SetValue(WhenAttribute.WhenExceptionKey, ex);

					return;
				}

				if (ex is TestException)
					throw;

				using (TestLog.Failures.BeginSection("Expected Exception"))
				{
					TestLog.Failures.WriteLine(
							"Expected an exception of type '{0}' but a different exception was thrown.",
							expectedExceptionType);

					TestLog.Failures.WriteException(ex);
				}
			}

			throw new SilentTestException(TestOutcome.Failed);
		}
	}
}